<?php
/**
 * Concentre Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Concentre
 * @package    Concentre_Daemon
 * @copyright  
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    
 * $Rev: 28 $
 * $Author: scramatte $
 * $Date: 2009-12-02 09:14:17 +0000 (Wed, 02 Dec 2009) $
 */

/**
 * @category   Concentre
 * @package    Concentre_Daemon
 * @copyright  
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Concentre_Daemon_Abstract
{

'PCNTL and POSIX extensions are required.'

    /**
     * 
     *
     * @var int
     */
    protected $_pid = 0;

    /**
     * 
     *
     * @var int
     */
    protected $_parentPid = 0;


    /**
     * 
     *
     * @var boolean
     */
    protected $_isChildren = false;


    /**
     * 
     *
     * @var boolean
     */
    protected $_isRunning = false;
    
    
    /**
     * Zend Logger.
     *
     * @var Zend_Log
     */
    protected $_logger;

   /**
     * Translation object
     * @var Zend_Translate
     */
    protected $_translator;

    /**
     * Contains the default options.
     *
     * @var array
     */
    protected  $_defaultOptions = array(
    	'uid' => 0,
    	'gid' => 0,
    	'home' => '/',
    	'pidfile' => '/tmp/daemon.pid',
    	'logger' => null,
    	'sleep' => 1000,
    	'foreground' => false,
    	'translator' => null
    );

    /**
     * Contains the options set by user.
     *
     * @var array
     */
    protected $_options = array(); 


    /**
     * Validation failure message template definitions
     *
     * @var array
     */
    protected $_messageTemplates = array();

    /**
     * Constructor
     *
     * @param array $options
     * @return void
     */
    public function __construct($options = array())
    {
        if (! extension_loaded('pcntl')
        or  ! extension_loaded('posix') ) {
            require_once 'Concentre/Daemon/Exception.php';
            throw new Concentre_Daemon_Exception('PCNTL and POSIX extensions are required.');
        }
        
        $this->setOptions($options);
        
        if (! $this->_options['logger'] instanceof Zend_Log) {
            require_once 'Zend/Log.php';
            $this->_logger = new Zend_Log();
        } else {
        	 $this->_logger = $this->_options['logger'];
        }

		$this->setTranslator($this->_options['translator']);
		
        $this->init();
        

    }

    /**
     * Initialize object
     *
     * Called from {@link __construct()} as final step of object instantiation.
     *
     * @return void 
     */
    public function init()
    {
    }

    /**
     * Clean object
     *
     * Called from {@link __destruct()} as final step of object destruction or manualy.
     *
     * @return void 
     */
    public function cleanup()
    {
    }

    /**
     * setOptions
     *
     * @param array $options
     * @return Concentre_Daemon_Abstract
     */
    public function setOptions($options)
    {
        if (! is_array($options) ) {
            if ($options instanceof Zend_Config ||
            	$options instanceof Zend_Console_Getopt) {
                $options = $options->toArray();
            } else {
                require_once 'Concentre/Daemon/Exception.php';
                throw new Concentre_Daemon_Exception('$options must be an array or Zend_Config object.');
            }
        }

        foreach ($options as $name => $value) {
            if (! array_key_exists($name, $this->_defaultOptions) ) {
                require_once 'Concentre/Daemon/Exception.php';
                throw new Concentre_Daemon_Exception("Unknown Concentre_Daemon option: {$name}");
            }

            $this->_options[$name] = $value;
        }

        return $this;
    }

    /**
     * Start
     *
     * @return void
     */
    public function start()
    {
        // daemonize
 		if (false == $this->_options['foreground']) {
		 	$this->_daemonize();
		}
		
        // processing
        $this->_isRunning = true;

        while ($this->_isRunning) {
            $this->_process();
            usleep($this->_options['sleep']);
        }
    }

    /**
     * Stop
     *
     * @return void
     */
    public function stop()
    {
        $this->_isRunning = false;
    }

    /**
     * Forks and daemonize the main process.
     *
     * @return void
     */
    protected function _daemonize()
    {
        // already a daemon
        if ( posix_getppid() ) {
        	$this->_logger->info("Daemon already running ...");
            return;
        }

		declare(ticks = 1);
		
        // register our signal handler
        pcntl_signal( SIGCHLD, array($this, '_signalHandler') );
        pcntl_signal( SIGUSR1, array($this, '_signalHandler') );
        pcntl_signal( SIGALRM, array($this, '_signalHandler') );

        // fork off the parent process
        $pid = pcntl_fork();

		if ($pid == -1) {
			$this->_logger->err("could not fork process.");
			
            require_once 'Concentre/Daemon/Exception.php';
            new Concentre_Daemon_Exception('Could not fork process.');
		} else if ($pid) { // we are the parent
			return;
		} else {
 			$this->_ischildren = true;	
			$this->_pid = posix_getpid();
			file_put_contents($this->_pidfile, $this->_pid);
		}

        // now we are executing as child process
        $this->_parentpid = posix_getppid();

        // cancel certain signals
        pcntl_signal(SIGCHLD, SIG_DFL); # a child process dies
        pcntl_signal(SIGTSTP, SIG_IGN); # various TTY signals
        pcntl_signal(SIGTTOU, SIG_IGN);
        pcntl_signal(SIGTTIN, SIG_IGN);
        pcntl_signal(SIGHUP, SIG_IGN); # ignore hangup signal
        pcntl_signal(SIGTERM, SIG_DFL); # die on SIGTERM
        
		//register_shutdown_function(array(&$this, 'releaseHandler'));

        // set identity
        if (!posix_setgid($this->_options['gid']) ||
        	!posix_setuid($this->_options['uid']) ) {
        	$this->_logger->err('could not set identity');
        	
            require_once 'Concentre/Daemon/Exception.php';
            new Concentre_Daemon_Exception('Could not set identity.');
        }

        // detach from terminal
        if ( -1 == posix_setsid() ) {
        	$this->_logger->err("could not detach from terminal");
        	
        	require_once 'Concentre/Daemon/Exception.php';
            new Concentre_Daemon_Exception('Could not detach from terminal.');
        }

        // change working directory to prevent from being locked
        if (!@chdir( $this->_options['home']) ) {
        	$this->_logger->err("could not change the working directory.");
        
        	require_once 'Concentre/Daemon/Exception.php';
            new Concentre_Daemon_Exception('Could not change the working directory.');
        }
		
        // change file-mode mask
        umask(0);

        // tell daddy that we are okay
        posix_kill($this->_parentpid, SIGUSR1);
    }


    /**
     *
     *
     * @return void
     */
	public function _releaseHandler() {
		if ($this->_ischildren && file_exists($this->_options['pidfile'])) {
			posix_kill($this->_pid, $this->_options['pidfile']);
		    @unlink( $this->_options['pidfile'] );
		}

	}
	
    /**
     *
     *
     * @return void
     */
	 protected function _sighandler($signo) {
		switch ($signo) {
			case SIGTERM:
	            $this->_logger->info("got SIGTERM");
				$this->stop();
				$this->cleanup();
				exit( 0 );
				break;
			case SIGHUP:
				$this->_logger->info("restarting process...");
				$this->cleanup();
				break;
			default:
		}
	}     
    
    /**
     *
     *
     * @return void
     */
    abstract protected function _process();


    /**
     * Constructs and returns a validation failure message with the given message key and value.
     *
     * Returns null if and only if $messageKey does not correspond to an existing template.
     *
     * If a translator is available and a translation exists for $messageKey,
     * the translation will be used.
     *
     * @param  string $messageKey
     * @param  string $value
     * @return string
     */
    protected function _createMessage($messageKey, $value)
    {
        if (!isset($this->_messageTemplates[$messageKey])) {
            return null;
        }

        $message = $this->_messageTemplates[$messageKey];

        if (null !== ($translator = $this->getTranslator())) {
            if ($translator->isTranslated($message)) {
                $message = $translator->translate($message);
            } elseif ($translator->isTranslated($messageKey)) {
                $message = $translator->translate($messageKey);
            }
        }

        return $message;
    }
    
    /**
     * Set translation object
     *
     * @param  Zend_Translate|Zend_Translate_Adapter|null $translator
     * @return Zend_Validate_Abstract
     */
    public function setTranslator($translator = null)
    {
        if ((null === $translator) || ($translator instanceof Zend_Translate_Adapter)) {
            $this->_translator = $translator;
        } elseif ($translator instanceof Zend_Translate) {
            $this->_translator = $translator->getAdapter();
        } else {
            require_once 'Zend/Validate/Exception.php';
            throw new Zend_Validate_Exception('Invalid translator specified');
        }
        return $this;
    }

    
}
